import {
	WebContainer,
	FileSystemTree,
	WebContainerProcess,
} from "@webcontainer/api";
import { Observable } from "./Observable";
import { ElMessage } from "element-plus";
import { VoidTypeFun } from "../interface";

/**
 * Web Container : https://webcontainers.io/api
 *
 *  因为 web container 是单例的，所以这里使用单例模式
 *  并且其不需要在页面展示，仅作为类工具提供接口即可
 *  这里的 fileTree = <FileSystemTree | Uint8Array | ArrayBuffer>{}
 *    是所有文件树的根源，只有在 Web Container 创建了文件，才能执行后续的操作，
 *    因此，这里才是所有页面文件结构的根源哈
 *    但是类中，不能直接调用 pinia 方法，因此，仅提供 getFileTree 方法，pinia 中提供 render 方法，在需要更新文件树的时候，请调用pinia render方法
 *  类中所有方法均为异步实现，请使用 async/await .then 的方式进行调用
 *
 *  1. initWebContainer 初始化WebContainer
 *  2. createFolder 创建文件夹
 *  3. createFile 创建文件
 *  4. writeFile 写入文件
 *  5. readFileContent 读取文件内容
 *  6. rename 重命名
 *  7. deleteFile 删除文件
 *  8. mountFileTree 挂载文件树 - 指的是初始化文件数（可用于导入文件夹、默认展示某文件）
 *  9. readPathResult 读取目录下的结果（用于将 We Container 的fileTree 转成 Pinia 的FileTreeData）
 *  10. execTerminal 执行终端命令
 *  11. output 封装统一的输出函数 - 监听容器输出
 */

const TRY_AGAIN_ERROR_MSG =
	"Failed to connect to the Web Container, please try again.";

type WebContainerFileTree = FileSystemTree | Uint8Array | ArrayBuffer;

class WebContainerAPI extends Observable {
	private url = ""; // 定义 WebContainer 启动后的url
	private port = 0; // 定义启动端口
	private boot = false; // 定义容器是否启动
	private fileTree = <WebContainerFileTree>{}; // 这个是 WebContainer 的文件树哈，与页面的文件树，还有些许差别 这里需要存储 content 内容字段，而页面上的不需要
	private container = <InstanceType<typeof WebContainer> | null>null; // container 容器
	private static MAX_RETRY = 3;
	private static RETRY_COUNT = 0;

	constructor() {
		super();
	}

	/**
	 * initWebContainer 启动容器并实现重连容器
	 */
	public initWebContainer() {
		this.url = "";
		this.port = 0;
		this.boot = false;
		this.fileTree = {};
		this.container = null;
		WebContainerAPI.MAX_RETRY = 3;
		WebContainerAPI.RETRY_COUNT = 0;

		return new Promise<void>(async (resolve, reject) => {
			for (let i = 0; i <= WebContainerAPI.MAX_RETRY; i++) {
				// 超过最大重试次数 则退出
				if (WebContainerAPI.RETRY_COUNT >= WebContainerAPI.MAX_RETRY) {
					console.error(TRY_AGAIN_ERROR_MSG);
					return reject(TRY_AGAIN_ERROR_MSG);
				}

				console.log("## Web Container Booting...");
				WebContainerAPI.RETRY_COUNT++; // 尝试连接次数加一

				try {
					this.container = await WebContainer.boot();

					// 如果这次连接不成功，并且不报错的情况下，直接进入下一次循环
					if (!this.container) continue;

					this.boot = true;

					this.container.on("error", (error) => {
						console.error("==>container error", error);
					});

					this.container.on("preview-message", (message) => {
						console.log("==>preview-message", message);
					});

					// 监听容器服务启动
					this.container.on(
						"server-ready",
						(port: number, url: string) => {
							this.port = port;
							this.url = url;
							this.emit("server-ready", { url });
						}
					);

					console.log("## Web Container Booted.");
					return resolve();
				} catch (error) {
					if (this.container) this.container.teardown();
					this.container = null;
					console.log("## Web Container Boot Failed, Retrying...");
				}
			}
		});
	}

	/**
	 * 创建文件夹
	 * @param path 文件夹路径
	 */
	public async createFolder(path: string) {
		if (!this.container || !this.boot) return;
		try {
			await this.container.fs.mkdir(path);
		} catch (error) {
			ElMessage.error("创建文件夹失败，请检查文件夹路径是否正确！");
		}
	}

	/**
	 * 创建文件
	 * @param path 文件路径
	 */
	public async createFile(path: string) {
		if (!this.container || !this.boot) return;
		try {
			await this.writeFile(path, ""); // 创建文件，内容为空
		} catch (error) {
			ElMessage.error("创建文件失败，请检查文件路径是否正确！");
		}
	}

	/**
	 * 写入文件 - 可用于创建文件，也可用于修改文件内容(该方法主要用于 monaco 编辑文件保存)
	 * @param path 写入文件路径
	 * @param data 写入文件内容 可传 ''
	 * @param options 文件写入配置
	 * @returns
	 */
	public async writeFile(
		path: string,
		data: string | Uint8Array | Buffer,
		options?: { encoding?: null | BufferEncoding } | null
	) {
		if (!this.container || !this.boot) return;
		try {
			await this.container.fs.writeFile(path, data, options);
		} catch (error) {
			console.error(error);
		}
	}

	/**
	 * 读取文件内容
	 * @param path 文件路径
	 */
	public async readFileContent(path: string) {
		if (!this.container || !this.boot) return;
		try {
			return await this.container.fs.readFile(path, "utf8");
		} catch (error) {
			console.error(error);
		}
	}

	/**
	 * 重命名
	 */
	async rename(oldpath: string, newpath: string) {
		if (!this.container || !this.boot) return;
		try {
			await this.container.fs.rename(oldpath, newpath);
		} catch (error) {
			console.error(error);
		}
	}

	/**
	 * 删除文件
	 * @param path 文件路径
	 */
	public async deleteFile(path: string) {
		if (!this.container || !this.boot) return;
		await this.container.fs.rm(path, { recursive: true });
	}

	/**
	 * 设置 Container 挂载的文件树
	 * @param fileTree
	 */

	public async mountFileTree(fileTree: WebContainerFileTree) {
		if (!this.container || !this.boot) return;
		this.fileTree = fileTree;
		await this.container?.mount(this.fileTree);
	}

	/**
	 * 读取指定文件下的所有内容 - 用于解析当前 webcontainer 下的文件结构
	 */
	public async readDir(path: string) {
		if (!this.container) return;
		return await this.container.fs.readdir(path, { withFileTypes: true });
	}

	/**
	 * 执行 terminal 命令
	 * @param cmd 命令
	 * @param fun 回调函数
	 */
	public async execTerminal(
		cmd: string,
		fun: VoidTypeFun<string | Uint8Array>
	) {
		if (!this.container || !this.boot) return;
		try {
			if (!this.container) return;
			const command = cmd.split(" "); // 这里是按空格进行分割
			const state = await this.container.spawn(
				command[0],
				command.slice(1)
			);
			// 如果是下载命令，则需要获取状态码
			if (command[1] === "install" || command[1] === "i") {
				const code = await state.exit;
				if (code !== 0) throw Error("install failed.");
			}
			// 输出
			this.output(state, fun);
		} catch (error) {
			console.error(error);
		}
	}

	/**
	 * 封装统一的输出函数 - 监听容器输出
	 * @param stdout WebContainerProcess
	 * @param fun 回调函数
	 */
	private output(stdout: WebContainerProcess, fun: (text: string) => void) {
		stdout.output.pipeTo(
			new WritableStream({
				write(data) {
					const encoder = new TextEncoder();
					const encodedData = encoder.encode(data);
					// 如果函数需要 ArrayBuffer，可以从 Uint8Array 获取它
					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
					// @ts-ignore
					const buffer: ArrayBuffer = encodedData.buffer;
					const decoder = new TextDecoder("utf-8"); // 使用 UTF-8 解码器处理数据流
					const text = decoder.decode(buffer, { stream: true }); // 将数据流解码为文本
					fun(text); // 传递解码后的文本
				},
			})
		);
	}

	/**
	 * getter
	 */
	public getURL = () => this.url;
	public getPort = () => this.port;
	public getFileTree = () => this.fileTree;
}

export default new WebContainerAPI();
